import UIKit
@available(iOSApplicationExtension, unavailable)
public extension IQKeyboardManager {
    typealias SizeBlock = (_ size: CGSize) -> Void
    private final class KeyboardSizeObserver: NSObject {
        weak var observer: NSObject?
        var sizeHandler: (_ size: CGSize) -> Void
        init(observer: NSObject?, sizeHandler: @escaping (_ size: CGSize) -> Void) {
            self.observer = observer
            self.sizeHandler = sizeHandler
        }
    }
    private struct AssociatedKeys {
        static var keyboardSizeObservers: Int = 0
        static var keyboardLastNotifySize: Int = 0
        static var keyboardShowing: Int = 0
        static var keyboardShowNotification: Int = 0
        static var keyboardFrame: Int = 0
        static var animationDuration: Int = 0
        static var animationCurve: Int = 0
    }
    private var keyboardLastNotifySize: CGSize {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.keyboardLastNotifySize) as? CGSize ?? .zero
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.keyboardLastNotifySize, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    private var keyboardSizeObservers: [AnyHashable: SizeBlock] {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.keyboardSizeObservers) as? [AnyHashable: SizeBlock] ?? [:]
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.keyboardSizeObservers, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    @objc func registerKeyboardSizeChange(identifier: AnyHashable, sizeHandler: @escaping SizeBlock) {
        keyboardSizeObservers[identifier] = sizeHandler
    }
    @objc func unregisterKeyboardSizeChange(identifier: AnyHashable) {
        keyboardSizeObservers[identifier] = nil
    }
    internal func notifyKeyboardSize(size: CGSize) {
        guard !size.equalTo(keyboardLastNotifySize) else {
            return
        }
        keyboardLastNotifySize = size
        for block in keyboardSizeObservers.values {
            block(size)
        }
    }
    @objc private(set) var keyboardShowing: Bool {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowing) as? Bool ?? false
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowing, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var keyboardShowNotification: Notification? {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.keyboardShowNotification) as? Notification
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.keyboardShowNotification, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    @objc private(set) var keyboardFrame: CGRect {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.keyboardFrame) as? CGRect ?? .zero
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.keyboardFrame, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var animationDuration: TimeInterval {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.animationDuration) as? TimeInterval ?? 0.25
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.animationDuration, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    internal var animationCurve: UIView.AnimationOptions {
        get {
            return objc_getAssociatedObject(self, &AssociatedKeys.animationCurve) as? UIView.AnimationOptions ?? .curveEaseOut
        }
        set(newValue) {
            objc_setAssociatedObject(self, &AssociatedKeys.animationCurve, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }
    @objc internal func keyboardWillShow(_ notification: Notification) {
        keyboardShowNotification = notification
        keyboardShowing = true
        let oldKBFrame = keyboardFrame
        if let info = notification.userInfo {
            if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
                animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
            } else {
                animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
            }
            if let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, duration != 0 {
                animationDuration = duration
            } else {
                animationDuration = 0.25
            }
            if let kbFrame = info[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
                keyboardFrame = kbFrame
                notifyKeyboardSize(size: keyboardFrame.size)
                showLog("UIKeyboard Frame: \(keyboardFrame)")
            }
        }
        guard privateIsEnabled() else {
            restorePosition()
            topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
            topViewBeginSafeAreaInsets = .zero
            return
        }
        let startTime = CACurrentMediaTime()
        showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
        showLog("Notification Object:\(notification.object ?? "NULL")")
        if let textFieldView = textFieldView, topViewBeginOrigin.equalTo(IQKeyboardManager.kIQCGPointInvalid) {
            rootViewController = textFieldView.parentContainerViewController()
            if let controller = rootViewController {
                if rootViewControllerWhilePopGestureRecognizerActive == controller {
                    topViewBeginOrigin = topViewBeginOriginWhilePopGestureRecognizerActive
                } else {
                    topViewBeginOrigin = controller.view.frame.origin
                    topViewBeginSafeAreaInsets = controller.view.safeAreaInsets
                }
                rootViewControllerWhilePopGestureRecognizerActive = nil
                topViewBeginOriginWhilePopGestureRecognizerActive = IQKeyboardManager.kIQCGPointInvalid
                self.showLog("Saving \(controller) beginning origin: \(self.topViewBeginOrigin)")
            }
        }
        if keyboardFrame.equalTo(oldKBFrame) == false {
            if keyboardShowing,
                let textFieldView = textFieldView,
                textFieldView.isAlertViewTextField() == false {
                self.adjustPosition()
            }
        }
        let elapsedTime = CACurrentMediaTime() - startTime
        showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
    }
    @objc internal func keyboardWillHide(_ notification: Notification?) {
        if notification != nil {
            keyboardShowNotification = nil
        }
        keyboardShowing = false
        if let info = notification?.userInfo {
            if let curve = info[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt {
                animationCurve = UIView.AnimationOptions(rawValue: curve).union(.beginFromCurrentState)
            } else {
                animationCurve = UIView.AnimationOptions.curveEaseOut.union(.beginFromCurrentState)
            }
            if let duration = info[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval, duration != 0 {
                animationDuration = duration
            } else {
                animationDuration = 0.25
            }
        }
        guard privateIsEnabled() else {
            return
        }
        let startTime = CACurrentMediaTime()
        showLog("⌨️>>>>> \(#function) started >>>>>", indentation: 1)
        showLog("Notification Object:\(notification?.object ?? "NULL")")
        if let lastScrollView = lastScrollView {
            UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
                if lastScrollView.contentInset != self.startingContentInsets {
                    self.showLog("Restoring contentInset to: \(self.startingContentInsets)")
                    lastScrollView.contentInset = self.startingContentInsets
                    lastScrollView.scrollIndicatorInsets = self.startingScrollIndicatorInsets
                }
                if lastScrollView.shouldRestoreScrollViewContentOffset, !lastScrollView.contentOffset.equalTo(self.startingContentOffset) {
                    self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
                    let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: lastScrollView) != nil 
                    if animatedContentOffset {
                        lastScrollView.setContentOffset(self.startingContentOffset, animated: UIView.areAnimationsEnabled)
                    } else {
                        lastScrollView.contentOffset = self.startingContentOffset
                    }
                }
                var superScrollView: UIScrollView? = lastScrollView
                while let scrollView = superScrollView {
                    let contentSize = CGSize(width: max(scrollView.contentSize.width, scrollView.frame.width), height: max(scrollView.contentSize.height, scrollView.frame.height))
                    let minimumY = contentSize.height - scrollView.frame.height
                    if minimumY < scrollView.contentOffset.y {
                        let newContentOffset = CGPoint(x: scrollView.contentOffset.x, y: minimumY)
                        if scrollView.contentOffset.equalTo(newContentOffset) == false {
                            let animatedContentOffset = self.textFieldView?.superviewOfClassType(UIStackView.self, belowView: scrollView) != nil 
                            if animatedContentOffset {
                                scrollView.setContentOffset(newContentOffset, animated: UIView.areAnimationsEnabled)
                            } else {
                                scrollView.contentOffset = newContentOffset
                            }
                            self.showLog("Restoring contentOffset to: \(self.startingContentOffset)")
                        }
                    }
                    superScrollView = scrollView.superviewOfClassType(UIScrollView.self) as? UIScrollView
                }
            })
        }
        restorePosition()
        lastScrollView = nil
        keyboardFrame = .zero
        notifyKeyboardSize(size: keyboardFrame.size)
        startingContentInsets = .zero
        startingScrollIndicatorInsets = .zero
        startingContentOffset = CGPoint.zero
        topViewBeginOrigin = IQKeyboardManager.kIQCGPointInvalid
        topViewBeginSafeAreaInsets = .zero
        let elapsedTime = CACurrentMediaTime() - startTime
        showLog("⌨️<<<<< \(#function) ended: \(elapsedTime) seconds <<<<<", indentation: -1)
    }
}
